windows10下的堆结构及unlink分析
前言
最近调试windows下堆的cve的时候发现对windows下的堆管理理解不够,对javascript堆分配和利用基础不够,由于windows下没有源码可以看,只能通过网上的博客和调试器自己学习。windows堆管理在不断更新,博客内容会有所偏差,接下来的笔记是windows10上的堆结构。
堆函数
函数声明等来源为百度百科。
HeapCreate
这个函数创建一个只有调用进程才能访问的私有堆。进程从虚拟地址空间里保留出一个连续的块,并且为这个块特定的初始部分分配物理空间。
HANDLE HeapCreate(DWORD flOptions ,DWORD dwInitialSize , DWORD dwMaxmumSize);
参数:
flOptions:堆的可选属性。这些标记影响以后对这个堆的函数操作,函数有—HeapAlloc , HeapFree , HeapReAlloc 和 HeapSize 。
下面给出在此可以指定的标记:
HEAP_NO_SERIALIAZE:指定当函数从堆里分配和释放空间时不互斥(不使用互斥锁)。当不指定该标记时默认为使用互斥。序列化允许多个线程操作同一个堆而不会错误。这个标记是可忽略的。
HEAP_SHARED_READONLY:这个标记指定这个堆只能由创建它的进程进行写操作,对其他进程是只读的。如果调用者不是可靠的,调用将会失败,错误代码ERROR_ACCESS_DENIDE 。
注解:
为了使用标记为HEAP_SHARED_READONLY的堆,运行在kernel mode(核心状态)是必须的。
dwInitialSize:堆的初始大小,单位为Bytes。这个值决定了分配给堆的初始物理空间大小。这个值将向上舍入知道下个page boundary(页界)。若需得到主机的页大小,使用GetSystemInfo 函数。
dwMaxmumSize:如果该参数是一个非零的值,它指定了这个堆的最大大小,单位为Bytes。该函数会向上舍入该值直到下个页界,然后为这个堆在进程的虚拟地址里保留舍入后大小的块。如果函数 HeapAlloc 和 HeapReAlloc 要求分配的空间超过参数 dwInitialSize 指定的大小,系统会分配额外的空间给该堆直到这个堆的最大大小。
If dwMaximumSize is nonzero, the heap cannot grow and an absolute limitation arises where all allocations are fulfilled within the specified heap unless there is not enough free space. (如果该参数非零,除非没有足够的空间,这个堆总可以增长到该大小)。如果该参数为零,那么该堆大小的唯一限制是可用的内存空间。分配大小超过 0x0018000 Bytes的空间总会失败,因为获得这么大的空间需要系统调用 VirtualAlloc 函数。需要使用大空间的应用,应该把该参数设置为零。
返回值:
成功:一个指向新创建的堆的指针。
失败:NULL
调用函数 GetLastError 获得更多的错误信息。
附注:
这个函数在调用进程里创建一个私有堆,进程可调用 HeapAlloc 函数分配内存空间。这些页在进程的虚拟空间内创建了一个块,在那里堆可以增长。
如果 HeapAlloc 函数请求的空间超过了现有的页大小,物理空间足够的话,额外的空间将会从已保留的空间里附加。
只有创建私有堆的进程才可以访问私有堆。
如果一个DLL(动态链接库)创建了一个私有堆,那么这个私有堆是在调用该DLL的进程的地址空间内,且仅该进程可访问。
系统会使用私有堆的一部分空间去储存堆的结构信息,所以,不是所有的堆内空间对进程来说是可用的。例如:HeapAlloc函数从一个最大大小为 64KB 的堆里申请 64KB 的空间,由于系统占用了一部分空间,这个请求通常会失败。
HeapAlloc
LPVOID HeapAlloc(
HANDLE hHeap,
DWORD dwFlags,
SIZE_T dwBytes,
);
hHeap:要分配堆的句柄,可以通过HeapCreate()函数或GetProcessHeap()函数获得。
dwFlags:堆分配时的可选参数,其值可以为以下的一种或多种。
HEAP_GENERATE_EXCEPTIONS:如果分配错误将会抛出异常,而不是返回NULL。异常值可能是STATUS_NO_MEMORY, 表示获得的内存容量不足,或是STATUS_ACCESS_VIOLATION,表示存取不合法。
HEAP_NO_SERIALIZE:不使用连续存取。
HEAP_ZERO_MEMORY:将分配的内存全部清零。
dwBytes:要分配堆的字节数。
HeapFree
BOOL HeapFree(
HANDLE hHeap,
DWORD dwFlags,
LPVOID lpMem
);
Heap Entry
Heap Entry类似于linux下的chunk。
前八个字节保存结构信息,类似chunk头,但是windows为了安全性,对前八个字节进行了加密,加密方式:与HEAP结构0x50偏移处八个字节异或(ps:此处HEAP 结构先理解为arena,后续再说),可以有效防止堆溢出。
由于被加密,windbg显示混乱:
于是通过x32dbg进行调试过程:
先把解密密钥提取出来,为:56 8B 12 F9 5B 64 00 00。
下面进行一次alloc堆的分配观察,Heap Entry结构:
charp = (char)HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 8000);
提取头部信息,异或解密得到eb 03 07 ef 90 00 00 18。
分配的大小为8000字节,得到头部信息大小为0x3eb,大小以8字节为单位,所以真实大小应该为0x3eb8=8024字节。
大小包含头部信息及最后16位的填充:
分配出来的堆块地址为0x019C0480:
相邻下一堆块地址为分配出来的堆块,地址为0x019C023d8。
验证发现0x019C023d8-0x019C0480=8024。
此时确定前两字节为size,通过查询第三字节为flag。
flag标志位
0×01 该块处于占用状态
0×02 该块存在额外描述
0×04 使用固定模式填充堆块
0×08 虚拟分配
0×10 该段最后一个堆块
此处分配的flag为07,表示该块处于占用状态,该块存在额外描述,使用固定模式填充堆块。
下一处空闲的堆块flag为0x4,使用固定模式填充堆块。
第四字节用于检测堆有效性的cookie。
提取相邻堆块解密头部为81 01 04 84 eb 03。
第5-6字节为pre_size,上一堆块的大小。验证,分配堆块的pre_size=0x90,0x908=0x480。0x019C0480-0x480=HeapCreate返回地基址。
第7字节堆块所在段的序号,未验证。
第8字节为UnusedBytes。未用到的字节,如分配出来的堆块头部及最后的16字节填充未使用,故第八字节为0x18。
相比较linux的堆漏洞利用,windows要多出一步信息泄露,下面是UAF的一个演示。通过堆的读和写得到加密密钥:
nt main()
{
HANDLE hHeap = HeapCreate(HEAP_NO_SERIALIZE, 10000, 40000);char * p = (char *)HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 8000);
bool bRetVal = HeapFree(hHeap, HEAP_NO_SERIALIZE, p);
HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 528);
void * pp = HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 528);
DWORD z = *(((DWORD *)p) + 0x112)^ 0xE20404E2;
DWORD z1 = *(((DWORD *)p) + 0x113)^ 0x45;
return 0;
}
HEAP_SEGMENT
一个heap结构有多个heap_segment,当堆的大小不足以分配时,创建新的堆段分配新的空间。堆段通过链表进行连接:
heap_segment分配在堆里,所以整体也算是一个HEAP_ENTRY,前8个字节为HEAP_ENTRY头部。
堆段的签名为0Xffeeffee;
偏移为0x10为堆段的链表;
0x18为堆段隶属的heap;
0x20为堆块分配的页的数目。
当需要分配的空间大小>这个数目*页时,会创建一个新的heap_segment并链入链表中。
FirstEntry,LastValidEntry 分别为堆段中第一个,及最后一个HEAP_ENTRY结构。
堆段中主要保存堆段的起始及范围、堆段隶属的heap和堆段的链表。
HEAP 结构
每个HEAP有一个HEAP结构,一个heap结构有多个heap_segment。
heap结构{
heap_segment
heap头部
}
heap结构在每个堆的起始地址,由每个堆的0号堆段和一个特殊结构拼接而成。由图可见堆段头部大小为0x40,前0x40字节属于0号堆段,0x40之后的heap结构用来保存堆的资产及必要信息。
+0x040 Flags : 2
+0x044 ForceFlags : 0
+0x048 CompatibilityFlags : 0
+0x04c EncodeFlagMask : 0x100000
+0x050 Encoding : _HEAP_ENTRY
+0x058 Interceptor : 0
+0x05c VirtualMemoryThreshold : 0xfe00
+0x060 Signature : 0xeeffeeff
+0x064 SegmentReserve : 0x1fd0000
+0x068 SegmentCommit : 0x2000
+0x06c DeCommitFreeBlockThreshold : 0x800
+0x070 DeCommitTotalFreeThreshold : 0x2000
+0x074 TotalFreeSize : 0x3c440
+0x078 MaximumAllocationSize : 0xfffdefff
+0x07c ProcessHeapsListIndex : 1
+0x07e HeaderValidateLength : 0x248
+0x080 HeaderValidateCopy : (null)
+0x084 NextAvailableTagIndex : 0
+0x086 MaximumTagIndex : 0
+0x088 TagEntries : (null)
+0x08c UCRList : _LIST_ENTRY [ 0x5a4ffe8 - 0x5a4ffe8 ]
+0x094 AlignRound : 0xf
+0x098 AlignMask : 0xfffffff8
+0x09c VirtualAllocdBlocks : _LIST_ENTRY [ 0x71009c - 0x71009c ]
+0x0a4 SegmentList : _LIST_ENTRY [ 0x710010 - 0x5760010 ]
+0x0ac AllocatorBackTraceIndex : 0
+0x0b0 NonDedicatedListLength : 0
+0x0b4 BlocksIndex : 0x00710260 Void
+0x0b8 UCRIndex : (null)
+0x0bc PseudoTagEntries : (null)
+0x0c0 FreeLists : _LIST_ENTRY [ 0x3c2b140 - 0x59ed748 ]
+0x0c8 LockVariable : 0x00710248 _HEAP_LOCK
+0x0cc CommitRoutine : 0x1bb2ba92 long +1bb2ba92
+0x0d0 StackTraceInitVar : _RTL_RUN_ONCE
+0x0d4 FrontEndHeap : 0x00150000 Void
+0x0d8 FrontHeapLockCount : 0
+0x0da FrontEndHeapType : 0x2 ‘’
+0x0db RequestedFrontEndHeapType : 0x2 ‘’
+0x0dc FrontEndHeapUsageData : 0x007161b0 -> 0
+0x0e0 FrontEndHeapMaximumIndex : 0x802
+0x0e2 FrontEndHeapStatusBitmap : [257] “???”
+0x1e4 Counters : _HEAP_COUNTERS
+0x240 TuningParameters : _HEAP_TUNING_PARAMETERS
+0x040 Flags 堆的flag由heapcreate时的flag参数控制,其中HEAP_GROWABLE(0x2)属性是默认的。且私有堆的flag要 or 0x1000。
创建时参数为4,flag为 2or 4 or 0x1000=0x1006
+0x050为之后解密HEAP_ENTRY头部的密钥;
+0x060 Signature : 0xeeffeeff heap结构头签名;
+0x078 MaximumAllocationSize 允许分配的最大空间,由于heapcreate时参数选择了0,这里允许分配整个地址空间大小;
+0x07c heap在peb堆数组中的索引;
+0x074 TotalFreeSize : 0x3c440 全部free堆块的总大小;
+0x07e HeaderValidateLength heap结构头的大小;
+0x0a4 SegmentList 堆段的链表,前向指针指向0号堆段,后向指针指向最后一个堆段;
+0x0c0 FreeLists 保存整个堆的空闲堆块,所有堆块的空闲堆块的集合。
unlink实验前言
实验尝试做windows下的unlink攻击,windows下有safeunlink保护,和glibc一样,若free x发生了unlink,要求x->fd->bk=x=x->bk->fd。
于是想进行一次类似于linux unlink的攻击,中间产生了异常事件。于是对windows的free函数进行了逆向分析。
unlink实验
void *pp;
void try1() {
HANDLE hHeap = HeapCreate(HEAP_GROWABLE, 1024, 10000);
DWORD zz = 1;
char *p = (char *)HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 8000);
bool bRetVal = HeapFree(hHeap, HEAP_NO_SERIALIZE, p);
HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 528);
void * ppp = HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 528);
DWORD z = *(((DWORD *)p) + 0x112) ^ 0xDF0404DF;
DWORD z1 = *(((DWORD *)p) + 0x113) ^ 0x45;
pp=HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 528);
printf("%x", pp);
HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 528);
HeapFree(hHeap, HEAP_NO_SERIALIZE, pp);
*(((DWORD *)p) + 0x112) = 0x41040045 ^ z;
*(((DWORD *)p) + 0x113) = 0x45 ^ z1;
*(((DWORD *)p) + 0x114) = DWORD(&pp -0x1);
*(((DWORD *)p) + 0x115) = DWORD(&pp);
HeapFree(hHeap, HEAP_NO_SERIALIZE, ppp);
}
int main()
{
try1();
printf("%x", pp);
return 0;
}
在实验过程中heapfree会调用一次raiseexception函数。若为调试器运行,则上面的unlink会攻击成功;若无调试器则不会成功。
得出结论,这样的unlink攻击不可利用,于是对windows下rtlfreeheap进行了逆向分析。
经过ida分析,得出真正的free过程在rtlpfreeheap函数中,但是此函数被保护不能直接f5。
rtlpfreeheap分析
第一步:解密heap_entry
这个函数就是windows下的堆free函数。
单步跟踪到此函数,对我们free heap的第一个内存操作。
edi寄存器保存的为我们heap结构的地址。
对heap偏移0x4c与0进行比较。
+0x04c EncodeFlagMask ,即判断此堆块是否进行过加密操作。若flag不为0,则进行下面的解密操作。
从偏移0x50处取出Encoding密钥异或解密heap_entry头部,未加密则直接跳转至解密后流程。
第二步:heap_entry检验
mov al,byte ptr ds:[ebx+2]
xor al,byte ptr ds:[ebx+1]
xor al,byte ptr ds:[ebx]
cmp byte ptr ds:[ebx+3],al
heap_entry偏移0,1,2相互异或操作,与偏移3进行对比。可得出偏移3为防止heap_entry被修改的cookie值。
防止off-by-one单字节修改heap_entry。
第三步:判断上一相邻堆块
通过自己pre_size找到上一堆块位置,计算过程:自己的地址-pre_size<<3
判断前一堆块是否为自己,即自己是否为堆中第一个堆块,若为自己直接转至第五步。
第四步:判断上一堆块是否free
判断过程:encodeflag>>14 and encode密钥 xor
若heap_entry未被加密 encodeflag>>14=0,0 and 密钥=0,0 xor flag不变。
若heap_entry被加密 encodeflag>>14=1,1 and 密钥=一位密钥。一位密钥 xor flag,flag最低位被解密。
通过test flag最低一位和1进行判断堆块是否free。
free则unlink,否则转第五步。
第五步:判断下一相邻堆块
通过自己size找到下一堆块位置,计算过程:自己的地址+size*8。
解密heap_entry,进行堆块有效性校验(ps:对于上一相邻堆块不进行解密和校验,应该是防止堆溢出)。
虽然校验过程与第一步代码不同,但根据单步调试。校验的实际效果是一样的。
heap_entry偏移0,1,2相互异或操作,与偏移3进行对比。
第六步:判断下一堆块是否freeHeapAlloc
判断过程同第四步一致,但是若可以进行unlink,还要再次对堆块进行校验(unlink前都会进行校验,不论前后)。
unlink过程
safe unlink
safe unlink的代码同linux下的保护如出一辙。
要求x->fd->bk=x=x->bk->fd。
失败原因
经过一段根据free堆块大小的判断及合并后大小的计算,来到失败处。
其实攻击失败原因很简单,unlink后会再次对相邻堆块进行完整性校验。这里我们的fd指针被修改成我们程序中内存一部分,并不能完成完成性校验,会调用raiseexcition(校验过程和上方一致故不贴出)。
此步正是unlink内存写入最后一个校验(如果堆块未启动加密的话,伪造heap_entry为0x0000000即可通过校验):
总结
经过unlink后会进行flag更新,链表更新。将堆块内容填充,最后加密heap_entry等步骤(这里就不继续跟踪了,最后的流程不长)。
经过分析可以得知,只有在堆块未加密情况下,且能伪造heap_entry为0x0000000才能进行利用(感叹windows的严谨和glibc的粗糙)。
看雪ID: sixty的梦想
https://bbs.pediy.com/user-770523.htm
本文由看雪论坛 sixty的梦想 原创
转载请注明来自看雪社区
热门技术文章推荐:
记录U3D逆向Assembly-CSharp-firstpass.dll解密
利用爬虫将电影网站打包成一个APP
使用Windbg&OllyDbg从头调试windows服务
一次Null-by-one的利用过程